/*
 * Created on Apr 14, 2005
 */
package org.flexdock.dockbar;

import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.WeakHashMap;

import javax.swing.JLayeredPane;
import javax.swing.SwingUtilities;

import org.flexdock.dockbar.activation.ActivationQueue;
import org.flexdock.dockbar.activation.ActiveDockableHandler;
import org.flexdock.dockbar.activation.Animation;
import org.flexdock.dockbar.event.ActivationListener;
import org.flexdock.dockbar.event.DockablePropertyChangeHandler;
import org.flexdock.dockbar.event.DockbarEvent;
import org.flexdock.dockbar.event.DockbarEventHandler;
import org.flexdock.dockbar.event.DockbarListener;
import org.flexdock.dockbar.event.DockbarTracker;
import org.flexdock.dockbar.layout.DockbarLayout;
import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.floating.frames.DockingFrame;
import org.flexdock.docking.props.PropertyChangeListenerFactory;
import org.flexdock.docking.state.DockingPath;
import org.flexdock.docking.state.DockingState;
import org.flexdock.docking.state.MinimizationManager;
import org.flexdock.event.EventManager;
import org.flexdock.perspective.RestorationManager;
import org.flexdock.util.RootWindow;
import org.flexdock.util.Utilities;

/**
 * @author Christopher Butler
 * @author Bobby Rosenberger
 * @author Mateusz Szczap
 */
public class DockbarManager {
	private static final WeakHashMap MANAGERS_BY_WINDOW = new WeakHashMap();
	public static final Integer DOCKBAR_LAYER = new Integer(JLayeredPane.PALETTE_LAYER.intValue() - 5);
	public static final int DEFAULT_EDGE = MinimizationManager.LEFT;

	private static String dockbarManagerClassName;

	private static DockbarManager currentManager;

	protected WeakReference windowRef;
	protected Dockbar leftBar;
	protected Dockbar rightBar;
	protected Dockbar bottomBar;
	protected ViewPane viewPane;

	protected DockbarLayout dockbarLayout;
	private ActivationListener activationListener;
	private HashMap dockables;

	private int activeEdge = MinimizationManager.UNSPECIFIED_LAYOUT_CONSTRAINT;
	private String activeDockableId;
	private boolean animating;
	private boolean dragging;


	static {
		Class c = DockingManager.class;
		EventManager.addHandler(new DockbarEventHandler());
		DockbarTracker.register();

		// setup to listen for Dockable property change events
		PropertyChangeListenerFactory.addFactory(new DockablePropertyChangeHandler.Factory());

		// update behavior of active Dockable changes
		EventManager.addListener(new ActiveDockableHandler());
	}

	public static DockbarManager getInstance(Component c) {
		RootWindow window = RootWindow.getRootContainer(c);
		return getInstance(window);
	}

	public static DockbarManager getInstance(RootWindow window) {
		if (window == null)
			return null;

		// DockingFrames should not be allowed to contain dockbars.
		// This may change in the future, but for now if our window is a
		// DockingFrame, reroute to its owner.
		Component root = window.getRootContainer();
		if (root instanceof DockingFrame) {
			root = ((DockingFrame) root).getOwner();
			return getInstance(root);
		}

		DockbarManager mgr = (DockbarManager) MANAGERS_BY_WINDOW.get(window);
		if (mgr == null) {
			mgr = createDockbarManager(window);
			synchronized (MANAGERS_BY_WINDOW) {
				MANAGERS_BY_WINDOW.put(window, mgr);
			}
			mgr.install();
		}

		if (currentManager == null)
			currentManager = mgr;

		return mgr;
	}

	/**
	 * Creates a new DockbarManager instance. In the case that a dockbarManager class name
	 * has been set the class will be instantiated by reflection. If no classname is set a
	 * org.flexdock.dockbar.DockbarManager will be created.
	 *
	 * @param window RootWindow for which the DockbarManager will be created
	 * @return new DockbarManager instance
	 * @see DockbarManager#setDockbarManager(String)
	 */
	private static DockbarManager createDockbarManager(RootWindow window) {
		if (dockbarManagerClassName == null)
			return new DockbarManager(window);

		DockbarManager mgr = null;
		try {
			Class clazz = Class.forName(dockbarManagerClassName);
			Constructor constructor = clazz.getConstructor(new Class[]{RootWindow.class});
			mgr = (DockbarManager) constructor.newInstance(new Object[]{window});
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return mgr;
	}

	/**
	 * Sets a custom DockbarManager class which will be used to create new DockbarManager
	 * instances.
	 *
	 * @param className Classname of your custom DockbarManager.
	 */
	public static void setDockbarManager(String className) {
		dockbarManagerClassName = className;
	}

	public static DockbarManager getCurrent(Dockable dockable) {
		if (dockable == null)
			return null;

		synchronized (MANAGERS_BY_WINDOW) {
			for (Iterator it = MANAGERS_BY_WINDOW.values().iterator(); it.hasNext(); ) {
				DockbarManager mgr = (DockbarManager) it.next();
				if (mgr.isOwner(dockable))
					return mgr;
			}
		}
		return null;
	}

	public static void windowChanged(Component newWindow) {
		currentManager = getInstance(newWindow);
	}

	public static DockbarManager getCurrent() {
		return currentManager;
	}

	public static void addListener(DockbarListener listener) {
		EventManager.addListener(listener);
	}

	public static void activate(String dockableId, boolean locked) {
		Dockable dockable = DockingManager.getDockable(dockableId);
		activate(dockable, locked);
	}

	public static void activate(Dockable dockable, boolean locked) {
		if (dockable == null)
			return;

		DockbarManager mgr = getCurrent(dockable);
		if (mgr == null || !mgr.contains(dockable))
			return;

		mgr.setActiveDockable(dockable);
		if (locked)
			mgr.getActivationListener().lockViewpane();
	}


	protected DockbarManager(RootWindow window) {
		dockbarLayout = new DockbarLayout(this);
		activationListener = new ActivationListener(this);

		leftBar = new Dockbar(this, MinimizationManager.LEFT);
		rightBar = new Dockbar(this, MinimizationManager.RIGHT);
		bottomBar = new StatusDockbar(this, MinimizationManager.BOTTOM);
		viewPane = new ViewPane(this);

		windowRef = new WeakReference(window);
		dockables = new HashMap();
	}

	public RootWindow getWindow() {
		return (RootWindow) windowRef.get();
	}


	protected void install() {
		RootWindow window = getWindow();
		if (window == null)
			return;

		JLayeredPane layerPane = window.getLayeredPane();
		boolean changed = install(leftBar, layerPane);
		changed = install(rightBar, layerPane) || changed;
		changed = install(bottomBar, layerPane) || changed;
		changed = install(viewPane, layerPane) || changed;

		if (changed) {
			layerPane.addComponentListener(new ComponentAdapter() {
				public void componentResized(ComponentEvent evt) {
					if (evt.getSource() instanceof JLayeredPane)
						revalidate();
				}
			});
		}
		revalidate();
	}

	private boolean install(Component c, JLayeredPane layerPane) {
		if (c.getParent() != layerPane) {
			if (c.getParent() != null)
				c.getParent().remove(c);
			layerPane.add(c, DOCKBAR_LAYER);
			return true;
		}
		return false;
	}


	public Dockbar getBottomBar() {
		return bottomBar;
	}

	public Dockbar getLeftBar() {
		return leftBar;
	}

	public Dockbar getRightBar() {
		return rightBar;
	}

	public ViewPane getViewPane() {
		return viewPane;
	}


	public void revalidate() {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				validate();
			}
		});

	}

	public void validate() {
		toggleDockbars();
		dockbarLayout.layout();
		viewPane.revalidate();
	}


	private void toggleDockbars() {
		leftBar.setVisible(leftBar.getComponentCount() != 0);
		rightBar.setVisible(rightBar.getComponentCount() != 0);
		bottomBar.setVisible(bottomBar.getComponentCount() != 0);
	}


	private int findDockbarEdge(Dockable dockable) {
		RootWindow window = RootWindow.getRootContainer(dockable.getComponent());
		if (window == null)
			return DEFAULT_EDGE;

		// get the dockable component and it's containing content pane
		Component cmp = dockable.getComponent();
		Container contentPane = window.getContentPane();

		// get the bounds of the content pane and dockable, translating the dockable into the
		// content pane's axes
		Rectangle contentRect = new Rectangle(0, 0, contentPane.getWidth(), contentPane.getHeight());
		Rectangle dockRect = SwingUtilities.convertRectangle(cmp.getParent(), cmp.getBounds(), contentPane);

		// get the center of the dockable
		Point dockCenter = new Point(dockRect.x + (dockRect.width / 2), dockRect.y + (dockRect.height / 2));
		// get the center left, right, and bottom points
		Point leftCenter = new Point(0, contentRect.height / 2);
		Point bottomCenter = new Point(contentRect.width / 2, contentRect.height);
		Point rightCenter = new Point(contentRect.width, contentRect.height / 2);

		// calculate the absolute distance from dockable center to each of the edge
		// center points.  whichever is the shortest, that is the edge the dockable is
		// 'closest' to and that will be the edge we'll return
		double min = Math.abs(dockCenter.distance(leftCenter));
		int edge = MinimizationManager.LEFT;
		double delta = Math.abs(dockCenter.distance(rightCenter));
		if (delta < min) {
			min = delta;
			edge = MinimizationManager.RIGHT;
		}
		delta = Math.abs(dockCenter.distance(bottomCenter));
		if (delta < min) {
			min = delta;
			edge = MinimizationManager.BOTTOM;
		}

		return edge;
	}

	public int getEdge(String dockableId) {
		Dockable dockable = DockingManager.getDockable(dockableId);
		return getEdge(dockable);
	}

	public int getEdge(Dockable dockable) {
		Dockbar dockbar = getDockbar(dockable);

		if (dockbar == leftBar)
			return MinimizationManager.LEFT;
		if (dockbar == rightBar)
			return MinimizationManager.RIGHT;
		if (dockbar == bottomBar)
			return MinimizationManager.BOTTOM;
		return MinimizationManager.UNSPECIFIED_LAYOUT_CONSTRAINT;
	}

	public Dockbar getDockbar(Dockable dockable) {
		if (dockable == null)
			return null;

		if (leftBar.contains(dockable))
			return leftBar;
		if (rightBar.contains(dockable))
			return rightBar;
		if (bottomBar.contains(dockable))
			return bottomBar;
		return null;
	}

	public Dockbar getDockbar(int edge) {
		edge = Dockbar.getValidOrientation(edge);
		switch (edge) {
			case MinimizationManager.RIGHT:
				return rightBar;
			case MinimizationManager.BOTTOM:
				return bottomBar;
			default:
				return leftBar;
		}
	}


	public void minimize(Dockable dockable) {
		if (dockable == null)
			return;

		int edge = DEFAULT_EDGE;
		RootWindow window = getWindow();
		if (window != null && DockingManager.isDocked(dockable)) {
			edge = findDockbarEdge(dockable);
		}

		minimize(dockable, edge);
	}

	public void minimize(Dockable dockable, int edge) {
		if (dockable == null)
			return;

		if (isDockingCancelled(dockable, edge))
			return;

		// install the dockable
		edge = Dockbar.getValidOrientation(edge);
		install(dockable, edge);

		// store the dockable id
		dockables.put(dockable.getPersistentId(), new Integer(edge));

		// send event notification
		DockbarEvent evt = new DockbarEvent(dockable, DockbarEvent.MINIMIZE_COMPLETED, edge);
		EventManager.dispatch(evt);
	}

	public void reAdd(Dockable dockable) {
		// can't re-add if the dockable is null, or we already contain it
		if (dockable == null || contains(dockable))
			return;

		Integer edge = (Integer) dockables.get(dockable.getPersistentId());
		if (edge != null)
			install(dockable, edge.intValue());
	}

	private void install(Dockable dockable, int edge) {
		Dockbar dockbar = getDockbar(edge);

		// undock the dockable
		DockingManager.undock(dockable);
		// place in the dockbar
		dockbar.dock(dockable);
		// make sure they can't drag the dockable while it's in the dockbar
		dockable.getDockingProperties().setDockingEnabled(false);
		// indicate that the dockable is minimized
		DockingState info = DockingManager.getLayoutManager().getDockingState(dockable);
		info.setMinimizedConstraint(edge);
		revalidate();
	}

	private boolean isDockingCancelled(Dockable dockable, int edge) {
		DockbarEvent evt = new DockbarEvent(dockable, DockbarEvent.MINIMIZE_STARTED, edge);
		EventManager.dispatch(evt);
		return evt.isConsumed();
	}


	public void restore(final Dockable dockable) {
		if (dockable == null) {
			return;
		}

		// now restore to the current layout
		final DockingState dockingState = DockingManager.getDockingState(dockable);
		final DockingPath dockingPath = dockingState.getPath();
		boolean restoreResult = false;

		if (dockingPath != null) {
			restoreResult = dockingPath.restore(dockable);
		} else {
			restoreResult = RestorationManager.getInstance().restore(dockable);
		}

		if (restoreResult) {
			// remove the dockable from the dockbar
			remove(dockable);
			// remove the dockable reference
			dockables.remove(dockable.getPersistentId());
		}
	}

	public boolean remove(Dockable dockable) {
		if (dockable == null)
			return false;

		if (getActiveDockable() == dockable)
			setActiveDockable((Dockable) null);

		Dockbar dockbar = getDockbar(dockable);
		if (dockbar == null)
			return false;

		DockingState info = DockingManager.getDockingState(dockable);
		dockbar.undock(dockable);
		info.setMinimizedConstraint(MinimizationManager.UNSPECIFIED_LAYOUT_CONSTRAINT);
		// restore drag capability to the dockable after removing
		// from the dockbar
		dockable.getDockingProperties().setDockingEnabled(true);
		revalidate();
		return true;
	}


	public int getActiveEdge() {
		synchronized (this) {
			return activeEdge;
		}
	}

	private void setActiveEdge(int edge) {
		synchronized (this) {
			activeEdge = edge;
		}
	}

	private Dockbar getActiveDockbar() {
		int edge = getActiveEdge();
		switch (edge) {
			case MinimizationManager.TOP:
				return bottomBar;
			case MinimizationManager.RIGHT:
				return rightBar;
			default:
				return leftBar;
		}
	}

	public String getActiveDockableId() {
		synchronized (this) {
			return activeDockableId;
		}
	}

	private void setActiveDockableId(String id) {
		synchronized (this) {
			activeDockableId = id;
		}
	}

	public Dockable getActiveDockable() {
		String dockingId = getActiveDockableId();
		Dockable dockable = DockingManager.getDockable(dockingId);
		return dockable;
	}


	public Cursor getResizeCursor() {
		return viewPane.getResizeCursor();
	}

	public boolean isActive() {
		return getActiveDockable() != null;
	}

	public void setActiveDockable(String dockableId) {
		Dockable dockable = DockingManager.getDockable(dockableId);
		setActiveDockable(dockable);
	}

	public void setActiveDockable(Dockable dockable) {
		// if we're not currently docked to any particular edge, then
		// we cannot activate the specified dockable.  instead, set the
		// active dockable to null
		final int newEdge = getEdge(dockable);
		if (newEdge == MinimizationManager.UNSPECIFIED_LAYOUT_CONSTRAINT)
			dockable = null;

		// check for dockable changes
		Dockable oldDockable = getActiveDockable();
		final String newDockableId = dockable == null ? null : dockable.getPersistentId();
		String currentlyActiveId = getActiveDockableId();
		boolean changed = Utilities.isChanged(currentlyActiveId, newDockableId);
		// check for edge changes
		changed = changed || newEdge != getActiveEdge();


		// if nothing has changed, then we're done
		if (changed) {
			viewPane.setLocked(false);
			setActiveEdge(newEdge);
			setActiveDockableId(newDockableId);
			startAnimation(oldDockable, dockable, newDockableId, newEdge);
		}
	}

	private void dispatchEvent(Dockable oldDockable, Dockable newDockable) {
		// dispatch to event listeners
		int evtType = DockbarEvent.EXPANDED;
		if (newDockable == null && oldDockable != null) {
			newDockable = oldDockable;
			evtType = DockbarEvent.COLLAPSED;
		}

		if (newDockable != null) {
			DockbarEvent evt = new DockbarEvent(newDockable, evtType, getActiveEdge());
			EventManager.dispatch(evt);
		}
	}

	private void startAnimation(final Dockable oldDockable, final Dockable newDockable, final String newDockableId, final int newEdge) {
		Animation deactivation = oldDockable == null ? null : new Animation(this, true);
		Runnable updater1 = new Runnable() {
			public void run() {
				setActiveEdge(newEdge);
				setActiveDockableId(newDockableId);
				viewPane.updateOrientation();
				viewPane.updateContents();
			}
		};
		Animation activation = newDockableId == null ? null : new Animation(this, false);
		Runnable updater2 = new Runnable() {
			public void run() {
				viewPane.setPrefSize(ViewPane.UNSPECIFIED_PREFERRED_SIZE);
				viewPane.updateOrientation();
				viewPane.updateContents();
				revalidate();

				// dispatch event notification
				dispatchEvent(oldDockable, newDockable);
			}
		};

		ActivationQueue queue = new ActivationQueue(this, deactivation, updater1, activation, updater2);
		queue.start();
	}

	public int getPreferredViewpaneSize() {
		return dockbarLayout.getDesiredViewpaneSize();
	}


	public boolean isAnimating() {
		return animating;
	}

	public void setAnimating(boolean animating) {
		this.animating = animating;
	}

	public boolean isDragging() {
		return dragging;
	}

	public void setDragging(boolean dragging) {
		this.dragging = dragging;
	}

	public ActivationListener getActivationListener() {
		return activationListener;
	}

	public boolean contains(Dockable dockable) {
		return getDockbar(dockable) != null;
	}

	private boolean isOwner(Dockable dockable) {
		return dockable == null ? false : dockables.containsKey(dockable.getPersistentId());
	}

	public DockbarLayout getLayout() {
		return dockbarLayout;
	}

}
